package framework.utils.http;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.GZIPInputStream;

import org.apache.log4j.Logger;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

import common.LogCategory;

import framework.server.Server;
import framework.utils.Xml;

/**
 * Http protocol implementation
 *
 * <p>
 * Change Log:
 * </p>
 * <ul>
 *  <li>v1.6.40 - able to connect via SSL.</li>
 *  <li>v1.6.38 - Added more xml contexts.</li>
 *  <li>v1.6.7a - Added ScreenTonic partner - no contenth lenght gif.</li>
 *  <li>v1.4.24 - Added web admin.</li>
 * </ul>
 *
 * @author Orlin Tomov
 * @version 0.2
 */
public abstract class Http {

	private static final Logger log = LogCategory.getCategory(Http.class);

	final public static int HTTP_PORT = 80;
	final public static int SSL_PORT = 443;

	final public static String HTTP = "http://";
	final public static String HTTPS = "https://";

	final public static int NO = 0;
	final public static int GZIP = 1;
	final public static int ZLIB = 2;

	/** max chars per line */
	private static final int MAX_LEN               = 100;
	/** max lines per single request/response */
	private static final int MAX_SIZE              = 200;

	public static final String CRLF                = "\r\n";
	public static final String HTTP_1_1            = "HTTP/1.1";
	public static final String HTTP_1_0            = "HTTP/1.0";
	public static final String POST                = "POST";
	public static final String GET                 = "GET";
	// resp
	public static final String Forbiden            = "403 Forbiden"; 
	public static final String OK                  = "200 OK";
	public static final String PARTIAL_CONTENT     = "206 Partial content";
	// agent
	public static final String SERVER              = "Server: " + Server.version;
	public static final String CLIENT              = "Client: " + Server.version;
	public static final String HOST                = "Host: ";
	public static final String USER_AGENT          = "User-Agent: ";
	// range
	public static final String CHUNKED             = "Transfer-Encoding: chunked";
	public static final String ACCEPT_RANGES       = "Accept-Ranges: bytes";
	public static final String CONTENT_RANGE       = "Content-Range: bytes ";
	public static final String RANGE_BYTES         = "Range: bytes="; // 0-13336
	public static final String CONTENT_LENGTH      = "Content-Length: ";
  // gzip
	public static final String ACCEPT_ENC_GZIP     = "Accept-Encoding: compress, gzip";
	public static final String ACCEPT_ENC_ZLIB     = "Accept-Encoding: compress, zlib";
	public static final String CONTENT_ENC_GZIP    = "Content-Encoding: gzip";
	public static final String CONTENT_ENC_ZLIB    = "Content-Encoding: zlib";
	// base64
	public static final String ACCEPT_ENC_BASE64   = "Accept-Transfer-Encoding: base64";
	public static final String ACCEPT_ENC_NO       = "Accept-Transfer-Encoding: no";
	public static final String CONTENT_ENC_BASE64  = "Content-Transfer-Encoding: base64";
	public static final String CONTENT_ENC_NO      = "Content-Transfer-Encoding: no";
	// type  
	public static final String CONTENT_TYPE        = "Content-Type: ";
	public static final String CONTENT_TYPE_HTML   = "Content-Type: text/html";
	public static final String CONTENT_TEXT_XML1   = "Content-Type: text/xml";
	public static final String CONTENT_TEXT_XML2   = "Content-type: text/xml";
	public static final String CONTENT_APP_XML     = "Content-Type: application/xml";
	public static final String CONTENT_APP_RSS_XML = "Content-Type: application/rss+xml";
	public static final String CONTENT_APP_RSS_XML2= "Content-type: application/rss+xml";
	public static final String CONTENT_APP_RDF_XML = "Content-Type: application/rdf+xml";
	public static final String CONTENT_APP_ATM_XML = "Content-Type: application/atom+xml";
	public static final String CONTENT_APP_OCT_STR = "Content-Type: application/octet-stream";
	public static final String CONTENT_APP_RSS_HTML= "Content-Type: application/rss+html";
	public static final String CONTENT_TEXT_PLAIN  = "Content-Type: text/plain";
	public static final String CONTENT_TEXT_RDF    = "Content-Type: text/rdf";
	public static final String CONTENT_TEXT_XML3   = "Content-Type: text/XML";
	public static final String CONTENT_TEXT_XML4   = "Content-Type: Text/xml";
	public static final String CONTENT_TEXT_XML5   = "Content-Type: Text/XML";
	public static final String CONTENT_XML         = "Content-Type: xml";

	// conn
	public static final String CONN_CLOSE          = "Connection: close";
	public static final String CONN_KEEP_ALIVE     = "Connection: Keep-Alive";

	/** http lines */
	protected List<Object> lines = new ArrayList<Object>();
	private int maxlen;

	/** constructor */
	protected Http() {
		maxlen = MAX_LEN;
	}

	/** constructor */
	protected Http(int maxlen) {
		this.maxlen = maxlen;
	}

	/**
	 * Adds a line
	 *
	 * @param line to add
	 */
	public void add(Object line) {
		lines.add(line);
	}

	/**
	 * Adds a lines
	 *
	 * @param lines to add
	 */
	public void addAll(List<Object> lines) {
		this.lines.addAll(lines);
	}

	public List<Object> parse(InputStream in) throws SAXException, IOException {
		return parse(in, new ArrayList<Object>());
	}

	/**
	 * Parse input stream
	 *
	 * @param in input stream to parse
	 *
	 * @return list of parsed lines
	 * @throws IOException
	 * @throws SAXException
	 */
	public List<Object> parse(InputStream in, List<Object> old) throws SAXException, IOException {
		String line = "";
		lines = old;
		while ((line = readLine(in, maxlen)) != null) {
			//System.out.println("http: " + line);
			lines.add(line);
			if (line.length() == 0) {
				break;
			}
			if (lines.size() > MAX_SIZE) {
				throw new IOException("Too many lines!");
			}
		}

		if (getLine(Http.CHUNKED).length() > 0) {
			return parseChunked(in);
		}
		if (getLine(Http.CONTENT_XML).length() > 0 ||
			getLine(Http.CONTENT_TEXT_XML1).length() > 0 ||
			getLine(Http.CONTENT_TEXT_XML2).length() > 0 ||
			getLine(Http.CONTENT_TEXT_XML3).length() > 0 ||
			getLine(Http.CONTENT_TEXT_XML4).length() > 0 ||
			getLine(Http.CONTENT_TEXT_XML5).length() > 0 ||
			getLine(Http.CONTENT_TEXT_PLAIN).length() > 0 ||
			getLine(Http.CONTENT_TEXT_RDF).length() > 0 ||
			getLine(Http.CONTENT_APP_XML).length() > 0 ||
			getLine(Http.CONTENT_APP_RSS_XML).length() > 0 ||
			getLine(Http.CONTENT_APP_RSS_XML2).length() > 0 ||
			getLine(Http.CONTENT_APP_RDF_XML).length() > 0 ||
			getLine(Http.CONTENT_APP_ATM_XML).length() > 0 ||
			getLine(Http.CONTENT_APP_OCT_STR).length() > 0 ||
			getLine(Http.CONTENT_APP_RSS_HTML).length() > 0) {
			Document document = null;
			if (getLine(Http.CONTENT_LENGTH).length() > 0) {
				if (getLine(Http.CONTENT_ENC_GZIP).length() > 0) {
					int contentLength = Integer.parseInt(getLine(Http.CONTENT_LENGTH).substring(Http.CONTENT_LENGTH.length()));
					parseContent(in, contentLength);
					lines.remove(lines.size() - 1);
					byte[] buff = (byte[])lines.remove(lines.size() - 1);
					document = Xml.parse(new GZIPInputStream(new ByteArrayInputStream(buff)));
				} else {
					document = Xml.parse(in);
				}
			} else {
				document = Xml.parse(in);
			}
			lines.add(document);
			lines.add("");
			return lines;
		}
		if (getLine(Http.CONTENT_LENGTH).length() > 0) {
			int contentLength = Integer.parseInt(getLine(Http.CONTENT_LENGTH).substring(Http.CONTENT_LENGTH.length()));
			return parseContent(in, contentLength);
		} else if (getLine("Content-Type: image/gif").length() > 0) {
			ByteArrayOutputStream bais=new ByteArrayOutputStream();
			byte[] buff=new byte[1];
			while (in.read(buff) >= 0) {
				bais.write(buff);
			}
			buff=bais.toByteArray();
			bais.close();
			lines.add(buff);
		}
		return lines;
	}

	/**
	 * Extracts single line contains a string
	 *
	 * @param value strign looking for
	 *
	 * @return found line with the string
	 */
	public String getLine(String value) {
		for (int i = 0; i < lines.size(); i++) {
			//System.out.println("obj:"+lines.get(i));
			if (lines.get(i) instanceof String) {
				String line = (String)lines.get(i);
				//System.out.println("line:"+line);
				if (line.indexOf(value) >= 0) {
					return line;
				}
			}
		}
		return "";
	}

	/**
	 * Reads a sinle line from the stream.
	 *
	 * @param in input stream
	 * @param maxCount max chars into the line
	 *
	 * @return read line from from the stream
	 *
	 * @throws IOException
	 */
	private String readLine(InputStream in, int maxCount) throws IOException {
		String line = "";
		int sign = -1;
		while ( (sign = in.read()) >= 0) {
			//Server.log.debug(Integer.toHexString(sign));
			if (sign == '\n') {
				break;
			} else if (sign != '\r') {
				line = line.concat(String.valueOf((char)sign));
				maxCount--;
				if (maxCount <= 0) {
					break;
				}
			}
		}
		return line;
	}

	/**
	 * Reads a byte data from the stream.
	 *
	 * @param in input stream
	 * @param maxCount max bytes into the data
	 *
	 * @return read data from the stream
	 *
	 * @throws IOException
	 */
	private byte[] readBytes(InputStream in, int maxCount) throws IOException {
		byte[] buff = new byte[maxCount];
		int sign = -1;
		//System.out.print(">> ");
		while ( (sign = in.read()) >= 0) {
			//Server.log.debug(Integer.toHexString(sign));
			buff[buff.length-maxCount] = (byte)sign;
			maxCount--;
			if (maxCount <= 0) {
				break;
			}
		}
		//System.out.println("<<");
		return buff;
	}

	/**
	 * Reads a byte data from the stream and followed end-of-line.
	 *
	 * @param in input stream
	 * @param maxCount max bytes into the data
	 *
	 * @return read data from the stream
	 *
	 * @throws IOException
	 */
	private byte[] readBytesLine(InputStream in, int maxCount) throws IOException {
		byte[] result = readBytes(in, maxCount);
		String line = readLine(in, 0); /** must be empty */
		/**/if (line.length() > 0) {
			/**/log.warn("ERROR: " + line);
		/**/}
		return result;
	}

	/**
	 * Parses chunched data
	 *
	 * @param in input stream
	 *
	 * @return parsed lines
	 *
	 * @throws IOException
	 */
	private List<Object> parseChunked(InputStream in) throws IOException  {
		int chunkSize = 0;
		do {
			if (chunkSize > 0) {
				byte[] buff = readBytesLine(in, chunkSize);
				//System.out.print("chunk:");
				//for (int i = 0; i < buff.length; i++) {
				//System.out.print(" " + Integer.toHexString(buff[i]));
				//}
				//System.out.println(".");
				lines.add(buff);
			}
			String chunkHeader = readLine(in, MAX_LEN);
			//System.out.println("chunkHeader: " + chunkHeader);
			lines.add(chunkHeader);
			try {
				chunkSize = Integer.parseInt(chunkHeader, 16);
			} catch (NumberFormatException exception) {
				throw new IOException(exception.getMessage());
			}
		} while (chunkSize > 0);
		String line = readLine(in, 0); /** must be empty */
		/**/if (line.length() > 0) {
			/**/log.warn("ERROR: " + line);
		/**/}
		lines.add(line);
		return lines;
	}

	/**
	 * Parse content byte data and followed empty line.
	 *
	 * @param in input stream
	 * @param contentLength
	 *
	 * @return parsed lines
	 *
	 * @throws IOException
	 */
	private List<Object> parseContent(InputStream in, int contentLength) throws IOException {
		int empty = 0;
		for (int i = 0; i < lines.size(); i++) {
			String line = (String)lines.get(i);
			if (line.length() == 0) {
				empty ++;
			}
		}
		byte[] buff;
		if (empty == 2) {
			buff = readBytes(in, contentLength);
		} else {
			buff = readBytesLine(in, contentLength);
		}
		lines.add(buff);

		if (empty < 2) {
			String line = readLine(in, 0); /** must be empty */
			/**/if (line.length() > 0) {
				/**/log.warn("ERROR: " + line);
			/**/}
			lines.add(line);
		}
		return lines;
	}

	/**
	 * Print out the lines
	 *
	 * @param out output stream
	 */
	public void print(OutputStream out) {
		PrintWriter writer = new PrintWriter(out);
		print(writer);
	}

	/**
	 * Print out the lines
	 *
	 * @param writer
	 */
	public void print(PrintWriter writer) {
		lines.add("");
		for (int i = 0; i < lines.size(); i++) {
			if (lines.get(i) instanceof String) {
				String line = (String)lines.get(i);
				//if (i < lines.size() - 2) {
				//  System.out.println("http: "+lines.get(i));
				//}
				writer.print(line+Http.CRLF);
			} else {
				//System.out.print("http: ");
				byte[] buff = (byte[])lines.get(i);
				for (int j = 0; j < buff.length; j++) {
					//System.out.print(Integer.toHexString(buff[j]) + " ");
					writer.print((char)buff[j]);
				}
				//System.out.println();
				writer.print(Http.CRLF);
			}
		}
		writer.flush();
	}
}
